using System;
using System.CodeDom.Compiler;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Text;
using Microsoft.CSharp;

namespace Common.DLR
{
    /// <summary>
    /// 
    /// </summary>
    public class CodeDomUtil
    {
        /// <summary></summary>
        public static object ExecuteMethod(MethodConstructInfo methodBuilder)
        {
        	object instance = CodeDomUtil.CompileMethod(methodBuilder);
			if(instance==null)
				throw new Exception("Unable to compile code");
            object returnObj = null;
            if (methodBuilder.Args != null && methodBuilder.Args.Count > 0)
            {
                List<object> argValues=new List<object>();
                foreach (MethodArgument arg in methodBuilder.Args)
                {
                    argValues.Add(arg.ArgValue);
                }
                returnObj =
                    instance.GetType().InvokeMember(methodBuilder.MethodName, BindingFlags.InvokeMethod, null, instance,
                                                    argValues.ToArray());
            }
            else
            {
                returnObj =
                    instance.GetType().InvokeMember(methodBuilder.MethodName, BindingFlags.InvokeMethod, null, instance, null);
            }
            return returnObj;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="methodBuilder"></param>
        /// <param name="globalAssemblies"></param>
        /// <param name="localAssemblies"></param>
        /// <returns></returns>
        public static object ExecuteMethod(MethodConstructInfo methodBuilder, List<string> globalAssemblies, List<string> localAssemblies)
        {
            object instance = CodeDomUtil.CompileMethod(methodBuilder, globalAssemblies, localAssemblies);
            if (instance == null)
                throw new Exception("Unable to compile code");
            object returnObj = null;
            if (methodBuilder.Args != null && methodBuilder.Args.Count > 0)
            {
                List<object> argValues = new List<object>();
                foreach (MethodArgument arg in methodBuilder.Args)
                {
                    argValues.Add(arg.ArgValue);
                }
                returnObj =
                    instance.GetType().InvokeMember(methodBuilder.MethodName, BindingFlags.InvokeMethod, null, instance,
                                                    argValues.ToArray());
            }
            else
            {
                returnObj =
                    instance.GetType().InvokeMember(methodBuilder.MethodName, BindingFlags.InvokeMethod, null, instance, null);
            }
            return returnObj;
        }

		/// <summary>
		/// 
		/// </summary>
		/// <param name="methodBuilder"></param>
		/// <returns></returns>
		public static object CompileMethod(MethodConstructInfo methodBuilder)
		{
			string className = "MyClass_" + DateTime.Now.Ticks.ToString();
		    CompilerParameters compilerParms = CodeDomUtil.AddReferencedAssemblies(null, null);
            
			compilerParms.GenerateInMemory = true;
		    string code = CodeDomUtil.GenerateCode(className, methodBuilder);
			

			CSharpCodeProvider codeProvider = new CSharpCodeProvider();
			CompilerResults compileResult = codeProvider.CompileAssemblyFromSource(compilerParms, code);
			if (compileResult.Errors.HasErrors)
			{
				throw new Exception(compileResult.Errors[0].ErrorNumber + ": " + compileResult.Errors[0].ErrorText);
			}
			Assembly localAssem = compileResult.CompiledAssembly;
			object instance = localAssem.CreateInstance("MyNamespace." + className);

			return instance;
		}

        /// <summary>
        /// 
        /// </summary>
        /// <param name="methodBuilder"></param>
        /// <param name="globalAssemblies"></param>
        /// <param name="localAssemblies"></param>
        /// <returns></returns>
        public static object CompileMethod(MethodConstructInfo methodBuilder, List<string> globalAssemblies, List<string> localAssemblies)
        {
            string className = "MyClass_" + DateTime.Now.Ticks.ToString();
            CompilerParameters compilerParms = CodeDomUtil.AddReferencedAssemblies(globalAssemblies, localAssemblies);

            compilerParms.GenerateInMemory = true;
            string code = CodeDomUtil.GenerateCode(className, methodBuilder);

            CSharpCodeProvider codeProvider = new CSharpCodeProvider();
            CompilerResults compileResult = codeProvider.CompileAssemblyFromSource(compilerParms, code);
            if (compileResult.Errors.HasErrors)
            {
                throw new Exception(compileResult.Errors[0].ErrorNumber + ": " + compileResult.Errors[0].ErrorText);
            }
            Assembly localAssem = compileResult.CompiledAssembly;
            object instance = localAssem.CreateInstance("MyNamespace." + className);

            return instance;
        }

        private static string GenerateCode(string className, MethodConstructInfo methodBuilder)
        {
            StringBuilder block = new StringBuilder();
            block.Append("using System;\n");
            block.Append("using System.Collections.Generic;\n");
            block.Append("using System.ComponentModel;\n");
            block.Append("using System.IO;\n");
            block.Append("using System.Text;\n");
            block.Append("using System.Data;\n");
            block.Append("using System.Drawing;\n");
            // block.Append("using System.Drawing;\n");
            block.Append("using System.Xml;\n\n");
            block.Append("using System.Text.RegularExpressions;\n");
            //block.Append("using System.Windows.Forms;\n");

            block.Append("namespace MyNamespace {\n");
            block.Append("public class " + className + " {\n");

            // method
            if (methodBuilder.ReturnType == null || methodBuilder.ReturnType == Type.Missing.GetType())
            {
                block.Append("public void " + methodBuilder.MethodName);
            }
            else
            {
                block.Append("public " + CodeDomUtil.GetTypeName(methodBuilder.ReturnType) + " " + methodBuilder.MethodName);
            }
            if (methodBuilder.Args != null && methodBuilder.Args.Count > 0)
            {
                block.Append("(");
                for (int i = 0; i < methodBuilder.Args.Count; i++)
                {
                    if (methodBuilder.Args[i].PassByReference)
                    {
                        block.Append("ref ");
                    }
                    block.Append(CodeDomUtil.GetTypeName(methodBuilder.Args[i].ArgType) +
                        " " + methodBuilder.Args[i].ArgName);
                    if (i < methodBuilder.Args.Count - 1)
                    {
                        block.Append(",");
                    }
                }
                block.Append(")");
            }
            else
            {
                block.Append("()");
            }
            block.Append("{" + Environment.NewLine + methodBuilder.MethodBody + Environment.NewLine + "}");
            block.Append("} \n");
            block.Append("} \n");

            return block.ToString();
        }

        private static string GetTypeName(Type type)
        {
            if (type.IsGenericType)
            {
                string genericTypeName = type.GetGenericTypeDefinition().Name;
                if (genericTypeName.IndexOf("`") > 0)
                {
                    genericTypeName = genericTypeName.Substring(0, genericTypeName.IndexOf("`"));
                }
                string paramName = string.Empty;
                Type[] paramTypes = type.GetGenericArguments();
                if (paramTypes != null && paramTypes.Length > 0)
                {
                    for (int i = 0; i < paramTypes.Length; i++)
                    {
                        paramName += paramTypes[i].Name;
                        if (i < paramTypes.Length - 1)
                            paramName += ",";
                    }
                }
                return genericTypeName + "<" + paramName + ">";
            }
            else
                return type.Name;
        }

        private static CompilerParameters AddReferencedAssemblies(List<string> globalAssemblies, List<string> localAssemblies)
        {
            CompilerParameters compilerParms = new CompilerParameters();
            if (globalAssemblies == null || !globalAssemblies.Contains("System.dll"))
                compilerParms.ReferencedAssemblies.Add("System.dll");
            if (globalAssemblies == null || !globalAssemblies.Contains("System.Xml.dll"))
                compilerParms.ReferencedAssemblies.Add("System.Xml.dll");
            if (globalAssemblies == null || !globalAssemblies.Contains("System.Data.dll"))
                compilerParms.ReferencedAssemblies.Add("System.Data.dll");
            if (globalAssemblies == null || !globalAssemblies.Contains("System.Drawing.dll"))
                compilerParms.ReferencedAssemblies.Add("System.Drawing.dll");

            if (globalAssemblies != null && globalAssemblies.Count > 0)
            {
                foreach (string globalAssemblyName in globalAssemblies)
                {
                    compilerParms.ReferencedAssemblies.Add(globalAssemblyName);
                }
            }

            string binFolderPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
            if (localAssemblies != null && localAssemblies.Count > 0)
            {
                foreach (string localAssemblyFileName in localAssemblies)
                {
                    if (File.Exists(localAssemblyFileName))
                    {
                        compilerParms.ReferencedAssemblies.Add(localAssemblyFileName);
                    }
                    else
                    {
                        string filePath = Path.Combine(binFolderPath, Path.GetFileName(localAssemblyFileName));
                        if (File.Exists(filePath))
                            compilerParms.ReferencedAssemblies.Add(filePath);
                    }
                }
            }

            return compilerParms;
        }
    }
}
